Completed
Push — master ( d61c7d...e7e6f1 )
by Jeff
03:32
created

Preload.preloadNext   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
rs 9.4285
cc 3
nc 3
nop 0
1
/** global: updateScreenUrl */
2
3
/**
4
 * Screen class constructor
5
 * @param {string} updateScreenUrl global screen update checks url
6
 */
7
function Screen(updateScreenUrl) {
8
  this.fields = [];
9
  this.url = updateScreenUrl;
10
  this.lastChanges = null;
11
  this.endAt = null;
12
  this.nextUrl = null;
13
  this.cache = new Preload();
14
}
15
16
/**
17
 * Ajax GET on updateScreenUrl to check lastChanges timestamp and reload if necessary
18
 */
19
Screen.prototype.checkUpdates = function() {
20
  var s = this;
21
  $.get(this.url, function(j) {
22
    if (j.success) {
23
      if (s.lastChanges == null) {
24
        s.lastChanges = j.data.lastChanges;
25
      } else if (s.lastChanges != j.data.lastChanges) {
26
        // Remote screen updated, we should reload as soon as possible
27
        s.nextUrl = null;
28
        s.reloadIn(0);
29
        return;
30
      }
31
32
      if (j.data.duration > 0) {
33
        // Setup next screen
34
        s.nextUrl = j.data.nextScreenUrl;
35
        s.reloadIn(j.data.duration * 1000);
36
      }
37
    } else if (j.message == 'Unauthorized') {
38
      // Cookie/session gone bad, try to refresh with full screen reload
39
      screen.reloadIn(0);
40
    }
41
  });
42
}
43
44
/**
45
 * Start Screen reload procedure, checking for every field timeout
46
 */
47
Screen.prototype.reloadIn = function(minDuration) {
48
  var endAt = Date.now() + minDuration;
49
  if (this.endAt != null && this.endAt < endAt) {
50
    // Already going to reload sooner than asked
51
    return;
52
  }
53
54
  if (this.cache.hasPreloadingContent(true)) {
55
    // Do not break preloading
56
    return;
57
  }
58
59
  this.endAt = Date.now() + minDuration;
60
  for (var i in this.fields) {
61
    if (!this.fields.hasOwnProperty(i)) {
62
      continue;
63
    }
64
    var f = this.fields[i];
65
    if (f.timeout && f.endAt > this.endAt) {
66
      // Always wait for content display end
67
      this.endAt = f.endAt;
68
    }
69
  }
70
71
  this.reloadOnTimeout();
72
}
73
74
/**
75
 * Check if we're past the screen.endAt timeout and reload if necessary
76
 * @return {boolean} going to reload
77
 */
78
Screen.prototype.reloadOnTimeout = function() {
79
  if (this.endAt != null && Date.now() >= this.endAt) {
80
    // No content to delay reload, do it now
81
    this.reloadNow();
82
    return true;
83
  }
84
85
  return false;
86
}
87
88
/**
89
 * Actual Screen reload/change screen action
90
 */
91
Screen.prototype.reloadNow = function() {
92
  if (this.nextUrl) {
93
    window.location = this.nextUrl;
94
  } else {
95
    window.location.reload();
96
  }
97
}
98
99
/**
100
 * Check every field for content
101
 * @param  {Content} data 
102
 * @return {boolean} content is displayed
103
 */
104
Screen.prototype.displaysData = function(data) {
105
  return this.fields.filter(function(field) {
106
    return field.current && field.current.data == data;
107
  }).length > 0;
108
}
109
110
/**
111
 * Trigger pickNext on all fields
112
 */
113
Screen.prototype.newContentTrigger = function() {
114
  for (var f in this.fields) {
115
    if (!this.fields.hasOwnProperty(f)) {
116
      continue;
117
    }
118
119
    this.fields[f].pickNextIfNecessary();
120
  }
121
}
122
123
/**
124
 * Loop through all fields for stuckiness state
125
 * @return {Boolean} are all fields stuck
126
 */
127
Screen.prototype.isAllFieldsStuck = function() {
128
  for (var f in this.fields) {
129
    if (!this.fields.hasOwnProperty(f)) {
130
      continue;
131
    }
132
133
    if (!this.fields[f].stuck && this.fields[f].canUpdate) {
134
      return false;
135
    }
136
  }
137
138
  return true;
139
}
140
141
142
/**
143
 * Content class constructor
144
 * @param {array} c content attributes
145
 */
146
function Content(c) {
147
  this.id = c.id;
148
  this.data = c.data;
149
  this.duration = c.duration * 1000;
150
  this.type = c.type;
151
  this.src = null;
152
153
  if (this.shouldPreload()) {
154
    this.queuePreload();
155
  }
156
}
157
158
/**
159
 * Check if content should be ajax preloaded
160
 * @return {boolean}
161
 */
162
Content.prototype.shouldPreload = function() {
163
  return this.canPreload() && !this.isPreloadingOrQueued() && !this.isPreloaded();
164
}
165
166
/**
167
 * Check if content has pre-loadable material
168
 * @return {boolean} 
169
 */
170
Content.prototype.canPreload = function() {
171
  return this.getResource() && this.type.search(/Video|Image|Agenda/) != -1;
172
}
173
174
/**
175
 * Check if content is displayable (preloaded and not too long)
176
 * @return {Boolean} can display
177
 */
178
Content.prototype.canDisplay = function() {
179
  return (screen.endAt == null || Date.now() + this.duration < screen.endAt) && this.isPreloaded();
180
}
181
182
/**
183
 * Extract url from contant data
184
 * @return {string} resource url
185
 */
186
Content.prototype.getResource = function() {
187
  if (this.src) {
188
    return this.src;
189
  }
190
  var srcMatch = this.data.match(/src="([^"]+)"/);
191
  if (!srcMatch) {
192
    // All preloadable content comes with a src attribute
193
    return false;
194
  }
195
  var src = srcMatch[1];
196
  if (src.indexOf('/') === 0) {
197
    src = window.location.origin + src;
198
  }
199
  if (src.indexOf('http') !== 0) {
200
    return false;
201
  }
202
  // Get rid of fragment
203
  src = src.replace(/#.*/g, '');
204
205
  this.src = src;
206
  return src;
207
}
208
209
/** Set content cache status
210
 * @param {string} state preload state
211
 */
212
Content.prototype.setPreloadState = function(state) {
213
  screen.cache.setState(this.getResource(), state);
214
}
215
216
/**
217
 * Check cache for preload status of content
218
 * @return {Boolean} 
219
 */
220
Content.prototype.isPreloaded = function() {
221
  if (!this.canPreload()) {
222
    return true;
223
  }
224
225
  return screen.cache.isPreloaded(this.getResource());
226
}
227
228
/**
229
 * Check cache for in progress or future preloading
230
 * @return {Boolean} is preloading
231
 */
232
Content.prototype.isPreloadingOrQueued = function() {
233
  return this.isPreloading() || this.isInPreloadQueue();
234
}
235
236
/**
237
 * Check cache for in progress preloading
238
 * @return {Boolean} is preloading
239
 */
240
Content.prototype.isPreloading = function() {
241
  return screen.cache.isPreloading(this.getResource());
242
}
243
244
/**
245
 * Check cache for queued preloading
246
 * @return {Boolean} is in preload queue
247
 */
248
Content.prototype.isInPreloadQueue = function() {
249
  return screen.cache.isInPreloadQueue(this.getResource());
250
}
251
252
/**
253
 * Call to preload content
254
 */
255
Content.prototype.preload = function() {
256
  var src = this.getResource();
257
  if (!src) {
258
    return;
259
  }
260
261
  screen.cache.preload(src);
262
}
263
264
/**
265
 * Preload content or add to preload queue
266
 */
267
Content.prototype.queuePreload = function() {
268
  var src = this.getResource();
269
  if (!src) {
270
    return;
271
  }
272
273
  if (screen.cache.hasPreloadingContent(false)) {
274
    this.setPreloadState(Preload.state.PRELOADING_QUEUE);
275
  } else {
276
    this.preload();
277
  }
278
}
279
280
281
/**
282
 * Preload class constructor
283
 * Build cache map
284
 */
285
function Preload() {
286
  this.cache = {};
287
}
288
289
/**
290
 * Set resource cache state
291
 * @param {string} res     resource url
292
 * @param {string|int} expires header or preload state
0 ignored issues
show
Documentation introduced by
The parameter expires does not exist. Did you maybe forget to remove this comment?
Loading history...
293
 */
294
Preload.prototype.setState = function(res, state) {
295
  this.cache[res] = state;
296
}
297
298
/**
299
 * Check resource cache for readyness state
300
 * @param  {string}  res resource url
301
 * @return {Boolean}     is preloaded
302
 */
303
Preload.prototype.isPreloaded = function(res) {
304
  return this.cache[res] === Preload.state.OK;
305
}
306
307
/**
308
 * Check resource cache for preloading state
309
 * @param  {string}  res resource url
310
 * @return {Boolean}     is currently preloading
311
 */
312
Preload.prototype.isPreloading = function(res) {
313
  return this.cache[res] === Preload.state.PRELOADING;
314
}
315
316
/**
317
 * Check resource cache for queued preloading state
318
 * @param  {string}  res resource url
319
 * @return {Boolean}     is in preload queue
320
 */
321
Preload.prototype.isInPreloadQueue = function(res) {
322
  return this.cache[res] === Preload.state.PRELOADING_QUEUE;
323
}
324
325
/**
326
 * Scan resource cache for preloading resources
327
 * @param  {Boolean}  withQueue also check preload queue
328
 * @return {Boolean}           has any resource preloading/in preload queue
329
 */
330
Preload.prototype.hasPreloadingContent = function(withQueue) {
331
  for (var res in this.cache) {
332
    if (!this.cache.hasOwnProperty(res)) {
333
      continue;
334
    }
335
336
    if (this.isPreloading(res) || (withQueue && this.isInPreloadQueue(res))) {
337
      return true;
338
    }
339
  }
340
341
  return false;
342
}
343
344
/**
345
 * Preload a resource by ajax get on the url
346
 * Check HTTP return state to validate proper cache
347
 * @param  {string} res resource url
348
 */
349
Preload.prototype.preload = function(res) {
350
  this.setState(res, Preload.state.PRELOADING);
351
352
  $.ajax(res).done(function(data, textStatus, jqXHR) {
0 ignored issues
show
Unused Code introduced by
The parameter jqXHR is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter data is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter textStatus is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
353
    // Preload success
354
    screen.cache.setState(res, Preload.state.OK);
355
    screen.newContentTrigger();
356
  }).fail(function() {
357
    // Preload failure
358
    screen.cache.setState(res, Preload.state.HTTP_FAIL);
359
  }).always(function() {
360
    screen.cache.preloadNext();
361
  });
362
}
363
364
/**
365
 * Try to preload next resource or trigger preload end event
366
 */
367
Preload.prototype.preloadNext = function() {
368
  var res = screen.cache.next();
369
  if (res) {
370
    // Preload ended, next resource
371
    screen.cache.preload(res);
372
    return;
373
  }
374
  // We've gone through all queued resources
375
  // Check if we should reload early
376
  if (screen.reloadOnTimeout()) {
377
    return;
378
  }
379
  // Trigger another update to calculate a proper screen.endAt value
380
  screen.checkUpdates();
381
}
382
383
/**
384
 * Get next resource to preload from queue
385
 * @return {string|null} next resource url
386
 */
387
Preload.prototype.next = function() {
388
  for (var res in this.cache) {
389
    if (!this.cache.hasOwnProperty(res)) {
390
      continue;
391
    }
392
393
    if (this.isInPreloadQueue(res)) {
394
      return res;
395
    }
396
  }
397
  return null;
398
}
399
400
/**
401
 * Preload states
402
 */
403
Preload.state = {
404
  PRELOADING: -2,
405
  PRELOADING_QUEUE: -3,
406
  HTTP_FAIL: -4,
407
  NO_EXPIRE_HEADER: -5,
408
  OK: -6,
409
}
410
411
412
/**
413
 * Field class constructor
414
 * @param {jQuery.Object} $f field object
415
 */
416
function Field($f) {
417
  this.$field = $f;
418
  this.id = $f.attr('data-id');
419
  this.url = $f.attr('data-url');
420
  this.types = $f.attr('data-types').split(' ');
421
  this.canUpdate = this.url != null;
422
  this.contents = [];
423
  this.previous = null;
424
  this.current = null;
425
  this.next = null;
426
  this.timeout = null;
427
  this.endAt = null;
428
  this.stuck = false;
429
}
430
431
/**
432
 * Retrieves contents from backend for this field
433
 */
434
Field.prototype.fetchContents = function() {
435
  if (!this.canUpdate) {
436
    return;
437
  }
438
439
  var f = this;
440
  $.get(this.url, function(j) {
441
    if (j.success) {
442
      f.contents = j.next.map(function(c) {
443
        return new Content(c);
444
      });
445
      f.pickNextIfNecessary();
446
    } else {
447
      f.setError(j.message || 'Error');
448
    }
449
  });
450
}
451
452
/**
453
 * Display error in field text
454
 */
455
Field.prototype.setError = function(err) {
456
  this.display(err);
457
}
458
459
/**
460
 * Randomize order
461
 */
462
Field.prototype.randomizeSortContents = function() {
463
  this.contents = this.contents.sort(function() {
464
    return Math.random() - 0.5;
465
  });
466
}
467
468
/**
469
 * PickNext if no content currently displayed and content is available
470
 */
471
Field.prototype.pickNextIfNecessary = function() {
472
  if (!this.timeout && this.contents.length) {
473
    this.pickNext();
474
  }
475
}
476
477
/**
478
 * Loop through field contents to pick next displayable content
479
 */
480
Field.prototype.pickNext = function() {
481
  if (screen.reloadOnTimeout()) {
482
    // Currently trying to reload, we're past threshold: reload now
483
    return;
484
  }
485
486
  this.previous = this.current;
487
  this.current = null;
488
  var previousData = this.previous && this.previous.data;
489
490
  this.next = this.pickRandomContent(previousData) || this.pickRandomContent(previousData, true);
491
492
  if (this.next) {
493
    // Overwrite field with newly picked content
494
    this.displayNext();
495
    this.stuck = false;
496
  } else {
497
    // I am stuck, don't know what to display
498
    this.stuck = true;
499
    // Check other fields for stuckiness state
500
    if (screen.isAllFieldsStuck() && !screen.cache.hasPreloadingContent(true)) {
501
      // Nothing to do. Give up, reload now
502
      screen.reloadNow();
503
    }
504
  }
505
}
506
507
/**
508
 * Loop through field contents for any displayable content
509
 * @param  {string}  previousData previous content data
510
 * @param {Boolean} anyUsable ignore constraints
511
 * @return {Content} random usable content
512
 */
513
Field.prototype.pickRandomContent = function(previousData, anyUsable) {
514
  this.randomizeSortContents();
515
  for (var i = 0; i < this.contents.length; i++) {
516
    var c = this.contents[i];
517
    // Skip too long or not preloaded content 
518
    if (!c.canDisplay()) {
519
      continue;
520
    }
521
522
    if (anyUsable) {
523
      // Ignore repeat & same content constraints if necessary
524
      return c;
525
    }
526
527
    // Avoid repeat same content
528
    if (c.data == previousData) {
529
      // Not enough content, display anyway
530
      if (this.contents.length < 2) {
531
        return c;
532
      }
533
      continue;
534
    }
535
536
    // Avoid same content than already displayed on other fields
537
    if (screen.displaysData(c.data)) {
538
      // Not enough content, display anyway
539
      if (this.contents.length < 3) {
540
        return c;
541
      }
542
      continue;
543
    }
544
545
    // Nice content. Display it.
546
    return c;
547
  }
548
  return null;
549
}
550
551
/**
552
 * Setup next content for field and display it
553
 */
554
Field.prototype.displayNext = function() {
555
  var f = this;
556
  if (this.next && this.next.duration > 0) {
557
    this.current = this.next
558
    this.next = null;
559
    this.display(this.current.data);
560
    if (this.timeout) {
561
      clearTimeout(this.timeout);
562
    }
563
    this.endAt = Date.now() + this.current.duration;
564
    this.timeout = setTimeout(function() {
565
      f.pickNext();
566
    }, this.current.duration);
567
  }
568
}
569
570
/**
571
 * Display data in field HTML
572
 * @param  {string} data 
573
 */
574
Field.prototype.display = function(data) {
575
  this.$field.html(data);
576
  this.$field.show();
577
  if (this.$field.text() != '') {
578
    this.$field.textfill({
579
      maxFontPixels: 0,
580
    });
581
  }
582
}
583
584
// Global screen instance
585
var screen = null;
586
587
/**
588
 * jQuery.load event
589
 * Initialize Screen and Fields
590
 * Setup updates interval timeouts
591
 */
592
function onLoad() {
593
  screen = new Screen(updateScreenUrl);
594
  // Init
595
  $('.field').each(function() {
596
    var f = new Field($(this));
597
    screen.fields.push(f);
598
    f.fetchContents();
599
  });
600
601
  if (screen.url) {
602
    // Setup content updates loop
603
    setInterval(function() {
604
      for (var f in screen.fields) {
605
        if (screen.fields.hasOwnProperty(f)) {
606
          screen.fields[f].fetchContents();
607
        }
608
      }
609
      screen.checkUpdates();
610
    }, 60000); // 1 minute is enough alongside preload queue end trigger
611
    screen.checkUpdates();
612
  }
613
}
614
615
// Run
616
$(onLoad);
617